-
Notifications
You must be signed in to change notification settings - Fork 378
IMDSv2 mTLS PoP: add best‑effort persisted binding‑cert cache (Windows CurrentUser\My) layered over in‑memory cache; per‑alias mutex; tests #5566
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Conversation
| /// Open the cert store and look at FriendlyName to see examples. | ||
| /// Wish we could paste a screenshot here... Maybe I can show it in code walkthroughs. | ||
| /// </summary> | ||
| internal static class FriendlyNameCodec |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
naming: MsiCertificateFriendlyNameEncoder ?
| /// Encodes/decodes the persisted X.509 FriendlyName for MSAL mTLS certs. | ||
| /// Format: "MSAL|alias=cacheKey|ep=endpointBase" | ||
| /// Open the cert store and look at FriendlyName to see examples. | ||
| /// Wish we could paste a screenshot here... Maybe I can show it in code walkthroughs. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
?
| { | ||
| /// <summary> | ||
| /// Encodes/decodes the persisted X.509 FriendlyName for MSAL mTLS certs. | ||
| /// Format: "MSAL|alias=cacheKey|ep=endpointBase" |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Can you give more examples here? This format is not clear.
| /// <param name="endpointBase"></param> | ||
| /// <param name="friendlyName"></param> | ||
| /// <returns></returns> | ||
| public static bool TryEncode(string alias, string endpointBase, out string friendlyName) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
nit: Avoid using these TryXYZ methods for cases where exceptions should be thrown.
| { | ||
| /// <summary> | ||
| /// Encodes/decodes the persisted X.509 FriendlyName for MSAL mTLS certs. | ||
| /// Format: "MSAL|alias=cacheKey|ep=endpointBase" |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
what is alias, cachekey, ep and endpoint base?
|
|
||
| /// <summary> | ||
| /// Checks for illegal characters in alias/endpointBase. | ||
| /// Endpoint itself comes from IMDS and is well-formed, but we still validate. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Why not throw?
| string cachedClientId; | ||
|
|
||
| // 1) Only lookup by cacheKey | ||
| // 1) In-memory cache first |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This 1) 2) 3) logic should be encapsualted in a different object, some sort of Cache abstraction.
| namespace Microsoft.Identity.Client.ManagedIdentity.V2 | ||
| { | ||
| /// <summary> | ||
| /// Cross-process lock based on a per-alias named mutex. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
What is an alias?
| Action<string> logVerbose = null) | ||
| { | ||
| var nameGlobal = GetMutexNameForAlias(alias, preferGlobal: true); | ||
| var nameLocal = GetMutexNameForAlias(alias, preferGlobal: false); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Explain in comment why you need 2 mutexes
|
|
||
| public static string GetMutexNameForAlias(string alias, bool preferGlobal = true) | ||
| { | ||
| string suffix = HashAlias(Canonicalize(alias)); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Do you need to hash? How long are the aliases? Maybe you can avoid hashing here.
| /// - No throws; persistence must not block token acquisition. | ||
| /// - Windows-only; FriendlyName semantics are undefined elsewhere. | ||
| /// </summary> | ||
| internal static class PersistentCertificateStore |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Are you overusing static? Everything seems static in this design. Does this not need mocking? And a logger?
| /// </summary> | ||
| internal static class PersistentCertificateStore | ||
| { | ||
| public static bool TryFind( |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is not a good interface for a cache - use Read / Write / Delete for caching.
| { | ||
| value = default; | ||
|
|
||
| if (!DesktopOsHelper.IsWindows()) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Why? The X509 stores exist on Linux as well.
| string bestEndpoint = null; | ||
| DateTime bestNotAfter = DateTime.MinValue; | ||
|
|
||
| foreach (var c in items) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
please use more meaningful variable names. Not c
| items = new X509Certificate2[store.Certificates.Count]; | ||
| store.Certificates.CopyTo(items, 0); | ||
| } | ||
| catch |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
All of these try / catch should log
| // CN (GUID) → canonical client_id | ||
| string cn = null; | ||
| try | ||
| { cn = best.GetNameInfo(X509NameType.SimpleName, false); } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Please format all docs and use proper indentation.
| string cn = null; | ||
| try | ||
| { cn = best.GetNameInfo(X509NameType.SimpleName, false); } | ||
| catch { } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
log all failures.
| /// - No throws; persistence must not block token acquisition. | ||
| /// - Windows-only; FriendlyName semantics are undefined elsewhere. | ||
| /// </summary> | ||
| internal static class PersistentCertificateStore |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
IMO, a better design would have been:
- a local in-memory cache logic
- a cert store logic
- an orchestrator
| string aliasCacheKey, | ||
| X509Certificate2 cert, | ||
| string endpointBase, | ||
| string clientId, // unused; CN is the source of truth on read |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If it's unused, why do you add it?
| // We only persist certs that can actually be used for mTLS later. | ||
| if (!cert.HasPrivateKey) | ||
| { | ||
| logger?.Verbose(() => "[PersistentCert] Not persisting: certificate has no private key."); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Seems like a warning to me? Or even an exception? Why would you persist certs without private key?
| /// <summary> | ||
| /// Removes expired entries (NotAfter less than now) for an alias from an open X509 store. | ||
| /// </summary> | ||
| internal static class StorePruner |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Doesn't seem worth having another (static!) class for 1 mehtod. Might as well move it to PersistentCertificaetStore? It's a tpyical cache op.
Fixes - Adds MSI v2 cert to the store (persistent cache)
Changes proposed in this request
The IMDSv2 mTLS PoP flow currently relies only on a process‑local cache. That means new processes (or app restarts) must re‑mint the binding certificate even when a valid one already exists on the machine. This PR layers a best‑effort, cross‑process persisted cache on top of the existing in‑memory cache to reduce reminting, while keeping token acquisition robust (never blocked by persistence).
Testing
unit testing
Performance impact
none
Documentation